Uurige Pythoni __slots__ funktsiooni, et drastiliselt vähendada mälukasutust ja suurendada atribuutidele juurdepääsu kiirust. Põhjalik juhend võrdlusaluste, kompromisside ja parimate tavadega.
Pythoni __slots__: Süvauurimine mälu optimeerimisse ja atribuutide kiirusesse
Tarkvaraarenduse maailmas on jõudlus ülimalt tähtis. Pythoni arendajate jaoks tähendab see sageli delikaatset tasakaalu keele uskumatult suure paindlikkuse ja ressursitõhususe vajaduse vahel. Üks levinumaid väljakutseid, eriti andmemahukates rakendustes, on mälukasutuse haldamine. Kui loote miljoneid või isegi miljardeid väikeseid objekte, loeb iga bait.
Siin tuleb mängu Pythoni vähem tuntud, kuid võimas funktsioon: __slots__
. Seda peetakse sageli maagiliseks lahenduseks mälu optimeerimiseks, kuid selle tõeline olemus on nüansirikkam. Kas see on ainult mälu säästmine? Kas see teeb teie koodi tõesti kiiremaks? Ja millised on selle kasutamise varjatud kulud?
See põhjalik juhend viib teid süvitsi Pythoni __slots__
funktsiooni. Me analüüsime, kuidas tavalised Pythoni objektid kulisside taga töötavad, hindame __slots__
reaalse maailma mõju mälule ja kiirusele, uurime selle üllatavaid keerukusi ja kompromisse ning pakume selge raamistiku otsustamaks, millal – ja millal mitte – seda võimsat optimeerimisvahendit kasutada.
Vaikimisi: Kuidas Pythoni objektid salvestavad atribuute funktsiooniga `__dict__`
Enne kui suudame hinnata, mida __slots__
teeb, peame esmalt mõistma, mida see asendab. Vaikimisi on iga Pythoni kohandatud klassi eksemplaril spetsiaalne atribuut nimega __dict__
. See on sõna otseses mõttes sõnastik, mis salvestab kõik eksemplari atribuudid.
Vaatame lihtsat näidet: klass 2D punkti esitamiseks.
import sys
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
# Looge eksemplar
p1 = Point2D(10, 20)
# Atribuudid salvestatakse __dict__ abil
print(p1.__dict__) # Väljund: {'x': 10, 'y': 20}
# Kontrollime __dict__ enda suurust
print(f"Punkti Point2D eksemplari __dict__ suurus: {sys.getsizeof(p1.__dict__)} baiti")
Väljund võib teie Pythoni versioonist ja süsteemi arhitektuurist veidi erineda (nt 64 baiti Python 3.10+ puhul väikese sõnastiku jaoks), kuid peamine järeldus on see, et sellel sõnastikul on oma mälujälg, mis on eraldi eksemplari objektist endast ja selle sisaldatavatest väärtustest.
Paindlikkuse jõud ja hind
See __dict__
lähenemisviis on Pythoni dünaamilisuse nurgakivi. See võimaldab teil igal ajal lisada eksemplarile uusi atribuute, mida sageli nimetatakse "monkey-patching"-uks:
# Lisage lennult uus atribuut
p1.z = 30
print(p1.__dict__) # Väljund: {'x': 10, 'y': 20, 'z': 30}
See paindlikkus on fantastiline kiireks arendamiseks ja teatud programmeerimismustrite jaoks. Kuid sellel on hind: mälu üldkulu.
Pythoni sõnastikud on kõrgelt optimeeritud, kuid on oma olemuselt keerukamad kui lihtsamad andmestruktuurid. Nad peavad säilitama räsiliikluse tabeli, et tagada kiire võtmeotsing, mis nõuab täiendavat mälu potentsiaalsete räsipõrkumiste haldamiseks ja tõhusaks suuruse muutmiseks. Kui loote miljoneid Point2D
eksemplare, millest igaĂĽks kannab oma __dict__
, koguneb see mälu üldkulu kiiresti.
Kujutage ette rakendust, mis töötleb 3D-mudelit 10 miljoni tipuga. Kui igal tipuobjektil on __dict__
suurusega 64 baiti, siis kulub 640 megabaiti mälu ainult sõnastikele, isegi enne tegelike täisarvu või ujukoma väärtuste arvesse võtmist, mida need salvestavad! See on probleem, mille lahendamiseks __slots__
loodi.
Tutvustame `__slots__`: mälu säästev alternatiiv
__slots__
on klassi muutuja, mis võimaldab teil selgesõnaliselt deklareerida atribuudid, mis eksemplaril on. Määratledes __slots__
, ĂĽtlete Pythonile sisuliselt: "Selle klassi eksemplaridel on ainult need konkreetsed atribuudid. Te ei pea nende jaoks __dict__
looma."
Sõnastiku asemel reserveerib Python eksemplari jaoks mälus kindla koguse ruumi, just piisavalt, et salvestada osutid deklareeritud atribuutide väärtustele, sarnaselt C structi või ennikuga.
Refaktoreerime oma Point2D
klassi, et kasutada __slots__
.
class SlottedPoint2D:
# Deklareerige eksemplari atribuudid
# See võib olla ennik (kõige tavalisem), loend või mis tahes stringide iteratiivne objekt.
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
Pealtnäha näeb see peaaegu identne välja. Kuid kulisside taga on kõik muutunud. __dict__
on kadunud.
p_slotted = SlottedPoint2D(10, 20)
# Katse pääseda __dict__ juurde põhjustab vea
try:
print(p_slotted.__dict__)
except AttributeError as e:
print(e) # Väljund: 'SlottedPoint2D' objektil puudub atribuut '__dict__'
Mälu säästu võrdlusalus
Tõeline "vau" moment saabub siis, kui võrdleme mälukasutust. Selle täpseks tegemiseks peame mõistma, kuidas objekti suurust mõõdetakse. sys.getsizeof()
teatab objekti põhisuuruse, kuid mitte nende asjade suurust, millele see viitab, nagu __dict__
.
import sys
# --- Tavaline klass ---
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
# --- Slotted klass ---
class SlottedPoint2D:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# Looge kumbagi üks eksemplar, et võrrelda
p_normal = Point2D(1, 2)
p_slotted = SlottedPoint2D(1, 2)
# Slotted eksemplari suurus on palju väiksem
# Tavaliselt on see objekti põhisuurus pluss osuti iga pesa jaoks.
size_slotted = sys.getsizeof(p_slotted)
# Tavalise eksemplari suurus sisaldab selle põhisuurust ja osutit selle __dict__ juurde.
# Kogusuurus on eksemplari suurus + __dict__ suurus.
size_normal = sys.getsizeof(p_normal) + sys.getsizeof(p_normal.__dict__)
print(f"Ăśhe SlottedPoint2D eksemplari suurus: {size_slotted} baiti")
print(f"Ühe Point2D eksemplari kogu mälujälg: {size_normal} baiti")
# Nüüd vaatame mõju skaalal
NUM_INSTANCES = 1_000_000
# Päris rakenduses kasutaksite sellist tööriista nagu memory_profiler
# protsessi kogu mälukasutuse mõõtmiseks.
# Saame säästu hinnata meie ühe eksemplari arvutuse põhjal.
size_diff_per_instance = size_normal - size_slotted
total_memory_saved = size_diff_per_instance * NUM_INSTANCES
print(f"\nLoote {NUM_INSTANCES:,} eksemplari...")
print(f"Mälu, mis on säästetud eksemplari kohta, kasutades __slots__: {size_diff_per_instance} baiti")
print(f"Hinnanguline kogu säästetud mälu: {total_memory_saved / (1024*1024):.2f} MB")
Tüüpilises 64-bitises süsteemis võite oodata mälusäästu 40-50% eksemplari kohta. Tavaline objekt võib võtta 16 baiti selle põhja jaoks + 8 baiti __dict__
osuti jaoks + 64 baiti tĂĽhja __dict__
jaoks, kokku 88 baiti. Slotted objekt kahe atribuudiga võib võtta ainult 32 baiti. See ~56-baidine erinevus eksemplari kohta tähendab 56 MB säästetud miljoni eksemplari kohta. See ei ole mikro-optimeerimine; see on põhjalik muudatus, mis võib muuta teostamatu rakenduse teostatavaks.
Teine lubadus: Kiirem atribuutidele juurdepääs
Lisaks mälu säästmisele reklaamitakse __slots__
ka jõudluse parandamiseks. Teooria on hea: väärtusele juurdepääs fikseeritud mälu nihkest (nagu massiivi indeks) on kiirem kui räsiotsingu sooritamine sõnastikus.
__dict__
Juurdepääs:obj.x
hõlmab sõnastiku otsingut võtme'x'
jaoks.__slots__
Juurdepääs:obj.x
hõlmab otsest mälu juurdepääsu kindlale pesale.
Kuid kui palju kiirem see praktikas on? Kasutame Pythoni sisseehitatud timeit
moodulit, et teada saada.
import timeit
# Seadistuskood, mida käitatakse üks kord enne ajastamist
SETUP_CODE = """
class Point2D:
def __init__(self, x, y):
self.x = x
self.y = y
class SlottedPoint2D:
__slots__ = 'x', 'y'
def __init__(self, x, y):
self.x = x
self.y = y
p_normal = Point2D(1, 2)
p_slotted = SlottedPoint2D(1, 2)
"""
# Testige atribuutide lugemist
read_normal = timeit.timeit("p_normal.x", setup=SETUP_CODE, number=10_000_000)
read_slotted = timeit.timeit("p_slotted.x", setup=SETUP_CODE, number=10_000_000)
print("--- Atribuutide lugemine ---")
print(f"Aeg __dict__ juurdepääsuks: {read_normal:.4f} sekundit")
print(f"Aeg __slots__ juurdepääsuks: {read_slotted:.4f} sekundit")
speedup = (read_normal - read_slotted) / read_normal * 100
print(f"Kiirendus: {speedup:.2f}%")
print("\n--- Atribuutide kirjutamine ---")
# Testige atribuutide kirjutamist
write_normal = timeit.timeit("p_normal.x = 3", setup=SETUP_CODE, number=10_000_000)
write_slotted = timeit.timeit("p_slotted.x = 3", setup=SETUP_CODE, number=10_000_000)
print(f"Aeg __dict__ juurdepääsuks: {write_normal:.4f} sekundit")
print(f"Aeg __slots__ juurdepääsuks: {write_slotted:.4f} sekundit")
speedup = (write_normal - write_slotted) / write_normal * 100
print(f"Kiirendus: {speedup:.2f}%")
Tulemused näitavad, et __slots__
on tõepoolest kiirem, kuid paranemine on tavaliselt vahemikus 10-20%. Kuigi see pole tähtsusetu, on see mälu säästmisega võrreldes palju vähem dramaatiline.
Peamine järeldus: Kasutage __slots__
peamiselt mälu optimeerimiseks. Pidage kiiruse paranemist teretulnud, kuid teisejärguliseks boonuseks. Jõudluse suurenemine on kõige olulisem tihedates silmustes arvutuslikult intensiivsetes algoritmides, kus atribuutidele juurdepääs toimub miljoneid kordi.
Kompromissid ja "Gotchas": Mida kaotate funktsiooniga `__slots__`
__slots__
ei ole tasuta lõuna. Jõudluse suurenemine toimub paindlikkuse hinnaga ja toob kaasa mõningaid keerukusi, eriti pärimise osas. Nende kompromisside mõistmine on ülioluline __slots__
tõhusaks kasutamiseks.
1. DĂĽnaamiliste atribuutide kadu
See on kõige olulisem tagajärg. Atribuutide eelnevalt määratlemisel kaotate võimaluse lisada käitusajal uusi.
p_slotted = SlottedPoint2D(10, 20)
# See toimib hästi
p_slotted.x = 100
# See ebaõnnestub
try:
p_slotted.z = 30 # 'z' ei olnud __slots__
except AttributeError as e:
print(e) # Väljund: 'SlottedPoint2D' objektil puudub atribuut 'z'
See käitumine võib olla funktsioon, mitte viga. See jõustab rangema objektimudeli, hoides ära juhusliku atribuudi loomise ja muutes klassi "kuju" paremini ennustatavaks. Kui aga teie disain tugineb dünaamilisele atribuutide määramisele, on __slots__
mittetoimiv.
2. `__dict__` ja `__weakref__` puudumine
Nagu oleme näinud, hoiab __slots__
ära __dict__
loomise. See võib olla problemaatiline, kui peate töötama teekidega või tööriistadega, mis tuginevad introspektsioonile __dict__
kaudu.
Samamoodi hoiab __slots__
ära ka __weakref__
automaatse loomise, mis on atribuut, mis on vajalik, et objekt oleks nõrgalt viidatav. Nõrgad viited on täiustatud mäluhaldustööriist, mida kasutatakse objektide jälgimiseks, takistamata nende prügikogumist.
Lahendus: Saate selgesõnaliselt lisada '__dict__'
ja '__weakref__'
oma __slots__
määratlusesse, kui neid vajate.
class HybridSlottedPoint:
# Saame mälu säästu x ja y jaoks, kuid meil on ikka __dict__ ja __weakref__
__slots__ = ('x', 'y', '__dict__', '__weakref__')
def __init__(self, x, y):
self.x = x
self.y = y
p_hybrid = HybridSlottedPoint(5, 10)
p_hybrid.z = 20 # See toimib nĂĽĂĽd, sest __dict__ on olemas!
print(p_hybrid.__dict__) # Väljund: {'z': 20}
import weakref
w_ref = weakref.ref(p_hybrid) # See toimib ka nĂĽĂĽd
print(w_ref)
'__dict__'
lisamine annab teile hĂĽbriidmudeli. Slotted atribuute (x
, y
) käsitletakse endiselt tõhusalt, samas kui kõik muud atribuudid paigutatakse __dict__
. See eitab mõningaid mälu sääste, kuid võib olla kasulik kompromiss, et säilitada paindlikkus, optimeerides samal ajal kõige tavalisemaid atribuute.
3. Pärimise keerukus
Siin võib __slots__
muutuda keeruliseks. Selle käitumine muutub sõltuvalt sellest, kuidas vanem- ja lapseklassid on määratletud.
Üksikpärimine
-
Kui vanemklassil on
__slots__
, kuid lapsel mitte: Lapsklass pärib vanema atribuutide jaoks slotted käitumise, kuid tal on ka oma__dict__
. See tähendab, et lapsklassi eksemplarid on suuremad kui vanema eksemplarid.class SlottedBase: __slots__ = ('a',) class DictChild(SlottedBase): # Siin pole __slots__ määratletud def __init__(self): self.a = 1 self.b = 2 # 'b' salvestatakse __dict__ abil c = DictChild() print(f"Lapsel on __dict__: {hasattr(c, '__dict__')}") # Väljund: True print(c.__dict__) # Väljund: {'b': 2}
-
Kui nii vanem- kui ka lapseklass määratlevad
__slots__
: Lapsklassil ei ole__dict__
. Selle efektiivne__slots__
on kombinatsioon selle enda__slots__
ja selle vanema__slots__
.class SlottedBase: __slots__ = ('a',) class SlottedChild(SlottedBase): __slots__ = ('b',) # Efektiivsed slots on ('a', 'b') def __init__(self): self.a = 1 self.b = 2 sc = SlottedChild() print(f"Lapsel on __dict__: {hasattr(sc, '__dict__')}") # Väljund: False try: sc.c = 3 # Põhjustab AttributeError except AttributeError as e: print(e)
__slots__
sisaldab atribuuti, mis on loetletud ka lapse__slots__
, on see ĂĽleliigne, kuid ĂĽldiselt kahjutu.
Mitmekordne pärimine
Mitmekordne pärimine koos __slots__
on miiniväli. Reeglid on ranged ja võivad põhjustada ootamatuid vigu.
-
Põhireegel: Selleks, et lapsklass saaks
__slots__
tõhusalt kasutada (st ilma__dict__
), peavad kõik selle vanemklassid samuti omama__slots__
. Kui isegi ĂĽhel vanemklassil puudub__slots__
(ja seega on__dict__
), on ka lapsklassil__dict__
. -
`TypeError` lõks: Lapsklass ei saa pärida mitmelt vanemklassilt, millel mõlemal on mittetühi
__slots__
.class SlotParentA: __slots__ = ('x',) class SlotParentB: __slots__ = ('y',) try: class ProblemChild(SlotParentA, SlotParentB): pass except TypeError as e: print(e) # Väljund: mitmel alusel on eksemplari paigutuse konflikt
Otsus: Millal ja millal mitte kasutada `__slots__`
Omades selget arusaama eelistest ja puudustest, saame luua praktilise otsustusraamistiku.
Rohelised lipud: Kasutage `__slots__`, kui...
- Loote tohutu hulga eksemplare. See on peamine kasutusjuhtum. Kui tegemist on miljonite objektidega, võib mälusääst olla erinevus rakenduse käivitamise ja krahhi vahel.
-
Objekti atribuudid on fikseeritud ja eelnevalt teada.
__slots__
sobib suurepäraselt andmestruktuuride, kirjete või lihtsate andmeobjektide jaoks, mille "kuju" ei muutu. - Olete mälupiirangutega keskkonnas. See hõlmab IoT-seadmeid, mobiilirakendusi või suure tihedusega servereid, kus iga megabait on väärtuslik.
-
Optimeerite jõudluse kitsaskohta. Kui profileerimine näitab, et atribuutidele juurdepääs tihedas silmuses on märkimisväärne aeglustumine, võib
__slots__
tagasihoidlik kiiruse suurendamine olla seda väärt.
Levinud näited:
- Sõlmed suures graafiku- või puustruktuuris.
- Osakesed fĂĽĂĽsika simulatsioonis.
- Objektid, mis esindavad ridu suurest andmebaasipäringust.
- Sündmus- või teateobjektid suure läbilaskevõimega süsteemis.
Punased lipud: Vältige `__slots__`, kui...
-
Paindlikkus on võtmetähtsusega. Kui teie klass on mõeldud üldiseks kasutamiseks või kui loodate atribuutide dünaamilisele lisamisele (monkey-patching), jääge vaike-
__dict__
juurde. -
Teie klass on osa avalikust API-st, mis on mõeldud teiste poolt alamklassideks jaotamiseks.
__slots__
kehtestamine baasklassile sunnib kõigile lapsklassidele peale piiranguid, mis võivad olla teie kasutajatele ebameeldiv üllatus. -
Te ei loo piisavalt eksemplare, et sellel oleks tähtsust. Kui teil on ainult mõni sada või tuhat eksemplari, on mälusääst tühine.
__slots__
rakendamine siin on ennatlik optimeerimine, mis lisab keerukust ilma reaalse kasuta. -
Tegelete keerukate mitmekordse pärimise hierarhiatega.
TypeError
piirangud võivad muuta__slots__
neis stsenaariumides rohkem vaeva kui see väärt on.
Kaasaegsed alternatiivid: Kas `__slots__` on endiselt parim valik?
Pythoni ökosüsteem on arenenud ja __slots__
pole enam ainus tööriist kergete objektide loomiseks. Kaasaegse Pythoni koodi puhul peaksite kaaluma neid suurepäraseid alternatiive.
`collections.namedtuple` ja `typing.NamedTuple`
Namedtuples on tehasefunktsioon enniku alamklasside loomiseks nimeliste väljadega. Need on uskumatult mälu säästvad (veelgi enam kui slotted objektid, kuna need on allosas ennikud) ja mis kõige tähtsam, muutumatud.
from typing import NamedTuple
# Loob muutumatu klassi koos tĂĽĂĽbiviidetega
class Point(NamedTuple):
x: int
y: int
p = Point(10, 20)
print(p.x) # 10
try:
p.x = 30 # Põhjustab AttributeError: atribuuti ei saa määrata
except AttributeError as e:
print(e)
Kui vajate muutumatut andmekonteinerit, on NamedTuple
sageli parem ja lihtsam valik kui slotted klass.
Mõlema maailma parim: `@dataclass(slots=True)`
Tutvustatud Python 3.7-s ja täiustatud Python 3.10-s, andmeklassid on mängumuutja. Nad genereerivad automaatselt meetodeid nagu __init__
, __repr__
ja __eq__
, vähendades drastiliselt boilerplati koodi.
Kriitiliselt on @dataclass
dekoraatoril argument slots
(saadaval alates Python 3.10; Python 3.8-3.9 jaoks on sama mugavuse jaoks vaja kolmanda osapoole teeki). Kui määrate slots=True
, genereerib andmeklass automaatselt __slots__
atribuudi, mis põhineb määratletud väljadel.
from dataclasses import dataclass
@dataclass(slots=True)
class DataPoint:
x: int
y: int
dp = DataPoint(10, 20)
print(dp) # Väljund: DataPoint(x=10, y=20) - kena repr tasuta!
print(hasattr(dp, '__dict__')) # Väljund: False - slots on lubatud!
See lähenemisviis annab teile kõigi maailmade parima:
- Loetavus ja lühidus: Palju vähem boilerplati kui käsitsi klassi määratlus.
- Mugavus: Automaatselt genereeritud erimeetodid säästavad teid tavalise boilerplati kirjutamisest.
- Jõudlus:
__slots__
täielik mälu ja kiiruse kasu. - Tüüpide ohutus: Integreerub suurepäraselt Pythoni tüübistikuga.
Python 3.10+ kirjutatud uue koodi puhul peaks `@dataclass(slots=True)` olema teie vaikevalik lihtsate, muudetavate ja mälu säästvate andmeid hoidvate klasside loomiseks.
Järeldus: Võimas tööriist konkreetse töö jaoks
__slots__
on tunnistus Pythoni disainifilosoofiast, mis pakub võimsaid tööriistu arendajatele, kes peavad jõudluse piire nihutama. See ei ole funktsioon, mida tuleks kasutada valimatult, vaid pigem terav ja täpne instrument konkreetse ja levinud probleemi lahendamiseks: paljude väikeste objektide kõrge mälukulu.
Teeme kokkuvõtte olulistest tõdedest __slots__
kohta:
- Selle peamine eelis on märkimisväärne mälukasutuse vähenemine, mis vähendab sageli eksemplaride suurust 40-50%. See on selle põhifunktsioon.
- See pakub teisest, tagasihoidlikumat, kiiruse suurenemist atribuutidele juurdepääsuks, tavaliselt umbes 10-20%.
- Peamine kompromiss on dünaamilise atribuudi määramise kadu, mis jõustab jäiga objektistruktuuri.
- See toob kaasa keerukuse pärimisega, nõudes hoolikat kujundust, eriti mitmekordse pärimise stsenaariumides.
-
Kaasaegses Pythonis on `@dataclass(slots=True)` sageli parem ja mugavam alternatiiv, mis ĂĽhendab
__slots__
eelised andmeklasside elegantsiga.
Siin kehtib optimeerimise kuldreegel: profileerige kõigepealt. Ärge puistake __slots__
kogu oma koodibaasi, lootes maagilisele kiiruse suurenemisele. Kasutage mälu profileerimise tööriistu, et tuvastada, millised objektid tarbivad kõige rohkem mälu. Kui leiate klassi, mida luuakse miljoneid kordi ja mis on peamine mälu röövel, siis – ja alles siis – on aeg haarata __slots__
järele. Mõistes selle jõudu ja ohte, saate seda tõhusalt kasutada tõhusamate ja skaleeritavamate Pythoni rakenduste loomiseks ülemaailmsele vaatajaskonnale.